#include <sys/types.h>
#include <sys/queue.h>
#include <assert.h>
#include <string.h>
#include <event.h>
#include <evmsg.h>

#include <sofia-sip/nua.h>
#include <sofia-sip/sip.h>
#include <sofia-sip/sip_header.h>
#include <sofia-sip/su_tag_io.h>

struct session {
  char *id;

  void *subscription;

  nua_handle_t *dialog;

  enum {
    callstate_idle = 0,
    callstate_offer_received,
    callstate_offer_sent,
    callstate_answer_received,
    callstate_answer_sent,
    callstate_established
  } state;

  /* copied when it's time */
  char *offer, *answer;

  TAILQ_ENTRY(session) next;
};

struct application {
  su_home_t home[1];
  su_root_t *root;
  nua_t *nua;

  su_timer_t *timer;

  TAILQ_HEAD(,session) sessions;
} app;

/* called from sofia-sip loop */
static void _callback(nua_event_t event,
  int status, char const *phrase,
  nua_t *nua, nua_magic_t *magic,
  nua_handle_t *nh, nua_hmagic_t *hmagic,
  sip_t const *sip, tagi_t tags[]);

/* to call libevent from sofia-sip loop */
static void _libevent_loop(su_root_magic_t *magic,
  su_timer_t *t, su_timer_arg_t *arg);

/* called from libevent pseudo-loop */
static void _create_outgoing_call(const char *channel,
  const char *type, const char *sender,
  struct evbuffer *buf, void *arg);
/* per-call dedicated topic */
static void _session_command(const char *channel,
  const char *type, const char *sender,
  struct evbuffer *buf, void *arg);

int main(int argc, char **argv) {
  TAILQ_INIT(&app.sessions);

  su_init();

  su_home_init(app.home);

  app.root = su_root_create(&app);
  assert(app.root != NULL);

  app.timer = su_timer_create(su_root_task(app.root), 30);
  assert(app.timer != NULL);

  su_timer_run(app.timer, _libevent_loop, NULL);

  app.nua = nua_create(app.root,
    _callback, NULL, TAG_END());
  assert(app.nua != NULL);

  nua_set_params(app.nua,
    NUTAG_AUTOALERT(0), NUTAG_SESSION_TIMER(0),
    NUTAG_AUTOANSWER(0), NUTAG_AUTOACK(0),
    NUTAG_MEDIA_ENABLE(0), TAG_END());

  event_init();

  evmsg_open("127.0.0.1", 8888, 0);
  evmsg_set_auth("sipua", "sipua");

  evmsg_subscribe("/sessions", "signalling/outgoing", "service", &_create_outgoing_call, NULL);

  su_root_run(app.root);

  return 0;
}

static void _libevent_loop(su_root_magic_t *magic,
  su_timer_t *t, su_timer_arg_t *arg) {
  event_loop(EVLOOP_NONBLOCK);
}

static int _id = 0;

static void _publish(const char *topic, const char *type,
  const char *fmt, ...) {
  struct evbuffer *buf;
  va_list ap;

  va_start(ap,fmt);

  buf = evbuffer_new();
  assert(buf != NULL);

  evbuffer_add_vprintf(buf, fmt, ap);

  evmsg_publish(topic, type, buf);

  evbuffer_free(buf);

  va_end(ap);
}

static void _callback(nua_event_t event,
  int status, char const *phrase,
  nua_t *nua, nua_magic_t *magic,
  nua_handle_t *nh, nua_hmagic_t *hmagic,
  sip_t const *sip, tagi_t tags[]) {
  struct session *s = hmagic;

  fprintf(stderr, "[sofia-sip] evt(%s) status(%d) '%s'\n",
    nua_event_name(event), status, phrase);
  tl_print(stdout, "tags", tags);

  switch (event) {
  case nua_i_ack:
    break;
  case nua_i_invite:
    /* create session */
    s = calloc(1, sizeof *s);
    assert(s != NULL);

    s->state = callstate_idle;

    s->dialog = nh;
    nua_handle_bind(nh, s);

    asprintf(&s->id, "/sessions/%08x", _id++);

    /* subscribe to signalling commands */
    s->subscription = evmsg_subscribe(s->id, "signalling/command", "service", &_session_command, s);
    assert(s->subscription != NULL);

    /* publish each new call */
    _publish("/sessions", "signalling/incoming", "%s", s->id);

    if (sip && sip->sip_payload && sip->sip_payload->pl_data) {
       s->state = callstate_offer_received;

       s->offer = strndup(sip->sip_payload->pl_data, sip->sip_payload->pl_len);
       assert(s->offer != NULL);

      _publish(s->id, "signalling/event", "incoming(%s)", s->offer);
    }

    nua_respond(s->dialog, 180, "Ringing", TAG_END());

    break;
  default:
    break;
  }
}

static int _extract_sdp(const char *msg, size_t len, char **content,
  size_t *content_len) {
  void *start, *end;
  start = memchr(msg, '(', len);
  end = memrchr(msg, ')', len);
  assert(start != NULL && end != NULL);

  *content = start + 1;
  *content_len = end - start - 1;

  return (*content_len > 0);
}

static void _session_command(const char *channel,
  const char *type, const char *sender,
  struct evbuffer *buf, void *arg) {
  struct session *s = arg;
  assert(s != NULL);
  const char *cmd = (const char *) EVBUFFER_DATA(buf);
  char *sdp = NULL;
  size_t sdp_len = 0;

  /* hangup */
  if (strncmp("hangup", cmd, 6) == 0) {
    printf("'%s' hangup\n", s->id);

    nua_bye(s->dialog, TAG_END());
  }
  /* answer(sdp), sdp may not be empty */
  else if (strncmp("answer", cmd, 6) == 0) {
    _extract_sdp(cmd, EVBUFFER_LENGTH(buf), &sdp, &sdp_len);

    assert(s->state == callstate_offer_received);

    s->answer = strndup(sdp, sdp_len);
    assert(s->answer != NULL);

    nua_respond(s->dialog, 200, "OK",
      SIPTAG_CONTENT_TYPE_STR("application/sdp"),
      SIPTAG_PAYLOAD_STR(s->answer),
      TAG_END());
  }
  /* offer(sdp), sdp may not be empty */
  else if (strncmp("offer", cmd, 5) == 0) {
    _extract_sdp(cmd, EVBUFFER_LENGTH(buf), &sdp, &sdp_len);

    assert(s->state == callstate_idle);

    s->offer = strndup(sdp, sdp_len);
    assert(s->offer != NULL);

    nua_respond(s->dialog, 200, "OK",
      SIPTAG_CONTENT_TYPE_STR("application/sdp"),
      SIPTAG_PAYLOAD_STR(s->offer),
      TAG_END());
  }
  /* update(sdp), sdp may be null */
/*  else if (strncmp("update", cmd, 6) == 0) {
  }*/
  else {
    fprintf(stderr, "dicarding unknown command '%.*s'\n",
      EVBUFFER_LENGTH(buf), cmd);
  }
}

static void _create_outgoing_call(const char *channel,
  const char *type, const char *sender,
  struct evbuffer *buf, void *arg) {
  struct session *s = calloc(1, sizeof *s);
  assert(s != NULL);
}
